package com.gitee.qdbp.base.shiro.filter;

import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.AccessControlFilter;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
import org.apache.shiro.web.util.SavedRequest;
import org.apache.shiro.web.util.WebUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.Assert;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.gitee.qdbp.able.exception.ServiceException;
import com.gitee.qdbp.able.result.ResponseMessage;
import com.gitee.qdbp.able.result.ResultCode;
import com.gitee.qdbp.base.i18n.IResultCodeI18nResolver;
import com.gitee.qdbp.base.shiro.token.FormParamToken;
import com.gitee.qdbp.base.shiro.validate.IParamValidator;
import com.gitee.qdbp.base.utils.WebTools;

/**
 * 登录提交过滤器, 支持JSON格式<br>
 * 配置了这个过滤器, 一律不允许访问, 必须提交用户名密码执行登录过程<br>
 * 登录成功, 如果是JSON格式, 返回用户信息, 否则跳转到成功页面<br>
 * 登录失败, 如果是JSON格式, 返回登录失败的原因, 否则跳转到失败页面
 *
 * <pre>
 * &lt;bean id="loginSubmissionFilter" class="{pkg}.JsonSupportLoginSubmissionFilter"&gt;
 *     &lt;property name="jsonHttpMessageConverter" ref="jsonHttpMessageConverter" /&gt;
 *     &lt;property name="authenticateDeniedUrl" value="error/500.jsp" /&gt;
 *     &lt;property name="paramValidators"&gt;
 *         &lt;list&gt;
 *             &lt;bean class="{pkg}.KaptchaValidator"&gt;
 *                 &lt;property name="captchaParam" value="captcha" /&gt;
 *             &lt;/bean&gt;
 *         &lt;/list&gt;
 *     &lt;/property&gt;
 * &lt;/bean&gt;
 * &lt;!-- disable captcha --&gt;
 * &lt;property name="captchaParam" value="DISABLED" /&gt;
 * </pre>
 *
 * @author zhaohuihua
 * @version 150918
 */
public class JsonSupportLoginSubmissionFilter extends FormAuthenticationFilter {

    private static final Logger log = LoggerFactory.getLogger(JsonSupportLoginSubmissionFilter.class);

    private JsonHttpMessageConverter<ResponseMessage> converter;

    @Autowired(required = false)
    private IResultCodeI18nResolver resultCodeI18nResolver;

    /** whether to stay compatible with HTTP 1.0 clients. **/
    protected boolean redirectHttp10Compatible = true;

    /** 登录失败的跳转页 **/
    private String authenticateDeniedUrl;

    /** 参数校验器 **/
    private List<IParamValidator> validators;

    /**
     * 一律不允许访问<br>
     * {@inheritDoc}
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        // 有这种可能性: login.do GET是登录页面, login.do POST是登录提交
        // 所以: URL是登录页面, 并且是GET请求, 则允许访问, 其他一律不允许访问
        return isLoginRequest(request, response) && isGetMethod(request);
    }

    /**
     * 访问被拒绝的处理, 执行登录过程<br>
     * {@inheritDoc}
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        Assert.notNull(converter, "JsonHttpMessageConverter must not be null");

        AuthenticationToken token = createToken(request, response);
        Assert.notNull(token, "createToken method implementation returned null.");

        // 验证请求参数
        if (validators != null && !validators.isEmpty()) {
            for (IParamValidator validator : validators) {
                try {
                    if (validator.doValidate(request, response) == false) {
                        break;
                    }
                } catch (ServiceException e) {
                    if (log.isTraceEnabled()) {
                        String name = validator.getClass().getSimpleName();
                        log.trace("Login submission parameter validate failed. [{}]", name);
                    }
                    return onLoginFailure(request, response, token, e);
                }
            }
        }

        String uri = WebUtils.toHttp(request).getRequestURI();

        if (log.isTraceEnabled()) {
            log.trace("Login submission detected. Attempting to execute login. [{}]", uri);
        }

        try {
            // 执行登录操作
            Subject subject = getSubject(request, response);
            subject.login(token);

            if (log.isDebugEnabled()) {
                log.debug("Login success, Account:{}", subject.getPrincipal());
            }
            // 登录成功的处理
            return onLoginSuccess(request, response, token, subject);
        } catch (AuthenticationException e) {
            if (log.isTraceEnabled()) {
                log.trace("Attempting to access a path which authenticate denied. [{}]", uri);
            }
            // 登录失败的处理
            return onLoginFailure(request, response, token, e);
        }
    }

    /**
     * 登录失败的处理
     *
     * @throws Exception
     **/
    protected boolean onLoginSuccess(ServletRequest request, ServletResponse response, AuthenticationToken token,
            Subject subject) throws Exception {

        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;

        if (converter.isJsonRequest(req)) {
            // 如果是JSON请求, 以JSON格式输出登录失败原因
            Object info = subject.getPrincipal();
            // 获取成功后跳转的URL
            String backurl = getBackUrl(req);
            JSONObject json = (JSONObject) JSON.toJSON(info);
            json.put("backurl", backurl);
            ResponseMessage rm = new ResponseMessage(json);
            if (resultCodeI18nResolver != null) {
                // 根据当时语言替换返回码对应的错误提示
                String message = resultCodeI18nResolver.getResultCodeMessage(rm.getCode());
                if (message != null) {
                    rm.setMessage(message);
                }
            }
            converter.write(rm, resp);
        } else {
            issueSuccessRedirect(request, response);
        }
        return false;
    }

    protected String getBackUrl(ServletRequest request) {
        SavedRequest sr = WebUtils.getSavedRequest(request);
        if (sr == null) {
            return getSuccessUrl();
        } else {
            return sr.getRequestUrl();
        }
    }

    /** 登录失败的处理 **/
    protected boolean onLoginFailure(ServletRequest request, ServletResponse response, AuthenticationToken token,
            Exception e) throws Exception {

        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;

        ResponseMessage rm = findExceptionCause(e);
        if (resultCodeI18nResolver != null) {
            // 根据当时语言替换返回码对应的错误提示
            String message = resultCodeI18nResolver.getResultCodeMessage(rm.getCode());
            if (message != null) {
                rm.setMessage(message);
            }
        }

        if (log.isWarnEnabled()) {
            log.warn("Login authenticate denied. Account:{}, {}, {}", token.getPrincipal(), rm, e.getMessage());
        }

        if (converter.isJsonRequest(req)) {
            // 如果是JSON请求, 以JSON格式输出登录失败原因
            converter.write(rm, resp);
        } else {
            Map<String, Object> result = (JSONObject) JSON.toJSON(rm);
            for (Entry<String, Object> entry : result.entrySet()) {
                req.setAttribute(entry.getKey(), entry.getValue());
            }

            String className = e.getClass().getName();
            request.setAttribute(getFailureKeyAttribute(), className);

            // 跳转至错误页面
            req.getRequestDispatcher(authenticateDeniedUrl).forward(req, resp);
        }
        return false;
    }

    protected ResponseMessage findExceptionCause(Exception e) {
        // 查找具体的错误原因
        if (e instanceof ServiceException) {
            return new ResponseMessage((ServiceException) e);
        } else if (e.getCause() instanceof ServiceException) {
            return new ResponseMessage((ServiceException) e.getCause());
        } else {
            if (log.isErrorEnabled()) {
                log.error("Catched exception", e);
            }
            return new ResponseMessage(ResultCode.SERVER_INNER_ERROR);
        }
    }

    @Override
    protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {

        FormParamToken token = new FormParamToken();

        String username = getUsername(request);
        String password = getPassword(request);
        token.setUsername(username);
        token.setPassword(password == null ? null : password.toCharArray());
        token.setRememberMe(isRememberMe(request));
        token.setHost(getHost(request));

        token.addParams(request.getParameterMap());
        return token;
    }

    @Override
    protected String getHost(ServletRequest request) {
        if (request instanceof HttpServletRequest) {
            return new WebTools((HttpServletRequest) request).getIpAddress();
        } else {
            return request.getRemoteAddr();
        }
    }

    private boolean isGetMethod(ServletRequest request) {
        return WebUtils.toHttp(request).getMethod().equalsIgnoreCase(GET_METHOD);
    }

    @Override
    protected String getPassword(ServletRequest request) {
        String pwd = request.getParameter(getPasswordParam());

        if (pwd == null || pwd.length() == 0) {
            return null;
        } else {
            return pwd;
        }
    }

    @Override
    protected void issueSuccessRedirect(ServletRequest request, ServletResponse response) throws Exception {
        String fallbackUrl = getSuccessUrl();
        redirectToSavedRequest(request, response, fallbackUrl);
    }

    protected void redirectToSavedRequest(ServletRequest request, ServletResponse response, String fallbackUrl)
            throws Exception {
        String successUrl = null;
        boolean contextRelative = true;
        SavedRequest savedRequest = WebUtils.getAndClearSavedRequest(request);
        if (savedRequest != null && savedRequest.getMethod().equalsIgnoreCase(AccessControlFilter.GET_METHOD)) {
            successUrl = savedRequest.getRequestUrl();
            contextRelative = false;
        }

        if (successUrl == null) {
            successUrl = fallbackUrl;
        }

        if (successUrl == null) {
            throw new IllegalStateException("Success URL not available via saved request or via the "
                    + "successUrlFallback method parameter. One of these must be non-null for "
                    + "issueSuccessRedirect() to work.");
        }

        WebUtils.issueRedirect(request, response, successUrl, null, contextRelative, redirectHttp10Compatible);
    }

    @Override
    protected void redirectToLogin(ServletRequest request, ServletResponse response) throws IOException {
        String loginUrl = getLoginUrl();
        WebUtils.issueRedirect(request, response, loginUrl, null, true, redirectHttp10Compatible);
    }

    /** whether to stay compatible with HTTP 1.0 clients. **/
    public boolean isRedirectHttp10Compatible() {
        return redirectHttp10Compatible;
    }

    /** whether to stay compatible with HTTP 1.0 clients. **/
    public void setRedirectHttp10Compatible(boolean redirectHttp10Compatible) {
        this.redirectHttp10Compatible = redirectHttp10Compatible;
    }

    /**
     * 设置JsonHttpMessageConverter
     *
     * @param converter JsonHttpMessageConverter
     */
    public void setJsonHttpMessageConverter(JsonHttpMessageConverter<ResponseMessage> converter) {
        this.converter = converter;
    }

    /**
     * 设置authenticateDeniedUrl
     *
     * @param authenticateDeniedUrl authenticateDeniedUrl
     */
    public void setAuthenticateDeniedUrl(String authenticateDeniedUrl) {
        this.authenticateDeniedUrl = authenticateDeniedUrl;
    }

    /** 设置参数校验器 **/
    public void setParamValidators(List<IParamValidator> validators) {
        this.validators = validators;
    }

    /** 结果返回码的国际化处理类 **/
    public void setResultCodeI18nResolver(IResultCodeI18nResolver resultCodeI18nResolver) {
        this.resultCodeI18nResolver = resultCodeI18nResolver;
    }
}
